/*
 * Copyright (c) 2008, Takashi Ito
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the authors nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "tansu_internal.h"
#include "tansu_socket.h"
#include "tansu_nwif.h"
#include "tansu_util.h"
#include "tansu_http.h"
#include "tansu_ssdp.h"
#include "tansu_upnp.h"

int
tansu_upnp_check_type(const char *dt1, const char *dt2)
{
    int i;
    int n;
    const char *names[] = {"urn", NULL, NULL, NULL};
    char *endptr;

    if (dt1 == NULL || dt2 == NULL)
        return -1;

    for (i = 0; i < 4; i++) {
        endptr = strchr(dt1, ':');
        n = endptr - dt1;

        if (dt1[0] == '\0' || strncmp(dt1, dt2, n + 1) != 0)
            return 1;

        if (names[i] && strncmp(names[i], dt1, n) != 0)
            return 1;

        dt1 += n + 1;
        dt2 += n + 1;
    }

    i = strtol(dt1, &endptr, 10);
    if (*endptr != '\0')
        return -3;

    n = strtol(dt2, &endptr, 10);
    if (*endptr != '\0')
        return -3;

    return i >= n ? 0 : 1;
}

int
tansu_upnp_count_3_2d_k(const struct tansu_device* device)
{
    int n = 3;
    struct tansu_device *embedded;
    struct tansu_service *service;

    STAILQ_FOREACH(embedded, &device->devices, next_embedded) {
        n += 2;
    }

    STAILQ_FOREACH(service, &device->services, next) {
        n++;
    }

    return n;
}

int
tansu_upnp_build_3_2d_k(const struct tansu_device *device,
                        const char **table,
                        int size)
{
    int n = 0;
    struct tansu_device *embedded;
    struct tansu_service *service;
    struct tdom_elem *elem;
    const char *tmp;

    if (size < 3)
        return 0;

    elem = tansu_dd_get_elem(device->dd->root, "device", 0);
    table[0] = "upnp:rootdevice";
    table[1] = tansu_dd_get_elem_value(elem, "UDN");
    table[2] = tansu_dd_get_elem_value(elem, "deviceType");
    size -= 3;
    n += 3;

    STAILQ_FOREACH(embedded, &device->devices, next_embedded) {
        if (size < 2)
            return n;
        elem = tansu_dd_get_elem(embedded->dd->root, "device", 0);
        tmp = tansu_dd_get_elem_value(elem, "UDN");
        TASSERT(tmp);
        table[n] = tmp;
        table[n + 1] = tmp;
        size -= 2;
        n += 2;
    }

    STAILQ_FOREACH(service, &device->services, next) {
        if (size < 1)
            return n;
        table[n] = service->type;
        size--;
        n++;
    }

    return n;
}

tansu_status_t
tansu_dd_set_udn(struct tdom_doc *dd, const char *udn)
{
    int ret;
    struct tdom_elem *elem;

    if (dd == NULL || udn == NULL)
        return TANSU_INVALID_ARGUMENT;

    elem = tdom_get_elem(dd->root, "device", TANSU_NS_UPNP_DEVICE, 0);
    ret = tdom_set_elem_value(elem, "UDN", udn, TANSU_NS_UPNP_DEVICE);

    return ret == 0 ? TANSU_SUCCESS : TANSU_FAILED;
}

struct tdom_elem *
tansu_dom_get_list_elem(struct tdom_elem *parent,
                        const char *ns,
                        const char *list_name,
                        const char *elem_name,
                        const char *field_name,
                        const char *name)
{
    struct tdom_elem *list_node;
    struct tdom_elem *elem;
    const char *tmp;
    int i;
    int n;

    if (name == NULL)
        return NULL;

    list_node = tdom_get_elem(parent, list_name, ns, 0);
    if (list_node == NULL)
        return NULL;

    n = tdom_count_elem(list_node, elem_name, ns);
    for (i = 0; i < n; i++) {
        elem = tdom_get_elem(list_node, elem_name, ns, i);
        tmp = tdom_get_value(tdom_get_elem(elem, field_name, ns, 0));
        if (tmp && strcmp(tmp, name) == 0)
            return elem;
    }

    return NULL;
}

static int
__build_device_types(const struct tansu_device *device,
                     const char *st,
                     const char **table,
                     int size)
{
    int count = 0;
    const char *dt;
    struct tansu_device *embedded;

    if (size < 1)
        return 0;

    dt = tansu_dd_get_device_type(device->dd);
    TASSERT(dt);

    if (tansu_upnp_check_type(dt, st) == 0) {
        table[count++] = dt;
        if (count >= size)
            return count;
    }

    STAILQ_FOREACH(embedded, &device->devices, next_embedded) {
        dt = tansu_dd_get_device_type(embedded->dd);
        if (tansu_upnp_check_type(dt, st) == 0) {
            table[count++] = dt;
            if (count >= size)
                break;
        }
    }

    return count;
}

static int
__build_service_types(const struct tansu_device *device,
                      const char *st,
                      const char **table,
                      int size)
{
    int count = 0;
    struct tansu_service *service;

    if (size < 1)
        return 0;

    STAILQ_FOREACH(service, &device->services, next) {
        if (tansu_upnp_check_type(service->type, st) == 0) {
            table[count++] = service->type;
            if (count >= size)
                break;
        }
    }

    return count;
}

static void
tansu_upnp_base_respond_msearch(struct tansu_upnp_base *base,
                                const struct tansu_ssdp_packet *packet,
                                enum tansu_ssdp_search_target type,
                                const char *st)
{
    struct tansu_device *device;
    int n;

    struct tdom_elem *elem;
    struct tansu_ssdp_response *res;
    const char *udn;

    STAILQ_FOREACH(device, &base->devices, next) {
        elem = tansu_dd_get_elem(device->dd->root, "device", 0);
        udn = tansu_dd_get_elem_value(elem, "UDN");
        TASSERT(udn);

        switch (type) {
        case TANSU_SSDP_ST_UPNP_DEVICE:
        case TANSU_SSDP_ST_VENDOR_DEVICE:
            if ((n = tansu_device_count_device_type(device, st)) == 0)
                continue;
            break;
        case TANSU_SSDP_ST_UPNP_SERVICE:
        case TANSU_SSDP_ST_VENDOR_SERVICE:
            if ((n = tansu_device_count_service_type(device, st)) == 0)
                continue;
            break;
        case TANSU_SSDP_ST_UUID:
            n = 1;
            if (strcmp(udn, st) != 0)
                continue;
            break;
        case TANSU_SSDP_ST_SSDP_ALL:
            n = tansu_upnp_count_3_2d_k(device);
            break;
        default:
            continue;
        }

        if ((res = tansu_ssdp_response_new(n)) == NULL)
            continue;

        switch (type) {
        case TANSU_SSDP_ST_UPNP_DEVICE:
        case TANSU_SSDP_ST_VENDOR_DEVICE:
            n = __build_device_types(device, st, res->st, res->st_size);
            break;
        case TANSU_SSDP_ST_UPNP_SERVICE:
        case TANSU_SSDP_ST_VENDOR_SERVICE:
            n = __build_service_types(device, st, res->st, res->st_size);
            break;
        case TANSU_SSDP_ST_UUID:
            res->st[0] = udn;
            break;
        case TANSU_SSDP_ST_SSDP_ALL:
            n = tansu_upnp_build_3_2d_k(device, res->st, res->st_size);
            break;
        default:
            continue;
        }

        TASSERT(n == res->st_size);

        res->udn = udn;
        res->current = 0;
        res->max_age = device->max_age;
        res->port = base->http_port;
        memcpy(&res->remote, &packet->remote, sizeof(res->remote));
        res->headers = device->headers;

        tansu_ssdp_server_reply(base->ssdp, res);
    }
}

void
tansu_upnp_base_ssdp_cb(const struct tansu_ssdp_packet *packet, void *arg)
{
    const char *st;
    struct tansu_upnp_base *base = (struct tansu_upnp_base *)arg;
    char *address = NULL;
    enum tansu_ssdp_search_target type;

    if (strcmp(packet->req.method, "M-SEARCH") != 0)
        return;

    if ((st = tansu_http_header_get(&packet->headers, "ST")) == NULL)
        return;

    if ((type = tansu_ssdp_get_search_target(st)) == TANSU_SSDP_ST_INVALID)
        return;

    tansu_upnp_base_respond_msearch(base, packet, type, st);
    free(address);
}


void
tansu_upnp_base_http_cb(struct evhttp_request *req, void *arg)
{
    struct tansu_device *device;
    struct tansu_upnp_base *base = (struct tansu_upnp_base *)arg;
    const char *udn;
    int len;

    TLOG_INFO(TLOG_UPNP, "%s(%s) called\n", __FUNCTION__, req->uri);

    if (req->uri[0] != '/')
        goto out;

    STAILQ_FOREACH(device, &base->devices, next) {
        udn = tansu_dd_get_udn(device->dd);
        if (udn == NULL)
            continue;
        len = strlen(udn);
        if (strncmp(&req->uri[1], udn, len) == 0) {
            tansu_device_http_cb(req, device, len);
            return;
        }
    }

out:
    evhttp_send_error(req, 404, "Not Found");
}

tansu_status_t
tansu_upnp_base_stop(struct tansu_upnp_base *base)
{
    if (base == NULL)
        return TANSU_INVALID_ARGUMENT;

    if (base->ssdp) {
        tansu_ssdp_server_stop(base->ssdp);
        tansu_ssdp_server_remove(base->ssdp, base->ssdp_listener);
        base->ssdp_listener = NULL;
        tansu_ssdp_server_delete(base->ssdp);
        base->ssdp = NULL;
    }
    if (base->http) {
        evhttp_free(base->http);
        base->http = NULL;
    }

    return TANSU_SUCCESS;
}

tansu_status_t
tansu_upnp_base_start(struct event_base *evbase,
                      struct tansu_upnp_base *base,
                      uint16_t http_port,
                      struct tansu_nwifs *nwifs)
{
    if (base == NULL)
        return TANSU_INVALID_ARGUMENT;

    if (base->ssdp || base->http)
        return TANSU_INVALID_STATE;

    if ((base->ssdp = tansu_ssdp_server_new()) == NULL)
        return TANSU_MEMORY_ERROR;

    if ((base->ssdp->server_name = strdup(base->server_name)) == NULL)
        goto out;

    if ((base->http = evhttp_new(evbase)) == NULL)
        goto out;
    if ((evhttp_bind_socket(base->http, "0.0.0.0", http_port)) != 0)
        goto out;

    base->evbase = evbase;

    base->http_port = http_port;

    if (tansu_ssdp_server_start(evbase, base->ssdp, nwifs) != TANSU_SUCCESS)
        goto out;

    tansu_upnp_base_notify_all(base);

    base->ssdp_listener = tansu_ssdp_server_add(base->ssdp,
                                                tansu_upnp_base_ssdp_cb,
                                                base);
    evhttp_set_gencb(base->http, tansu_upnp_base_http_cb, base);

    return TANSU_SUCCESS;

out:
    tansu_upnp_base_stop(base);
    return TANSU_MEMORY_ERROR;
}

struct tansu_upnp_base *
tansu_upnp_base_new()
{
    struct tansu_upnp_base *base;

    if ((base = malloc(sizeof(struct tansu_upnp_base)))) {
        base->nwif_listener = NULL;
        base->ssdp = NULL;
        base->ssdp_listener = NULL;
        base->evbase = NULL;
        base->http = NULL;
        base->http_port = 0;
        STAILQ_INIT(&base->devices);
        base->server_name = NULL;
    }

    return base;
}

void
tansu_upnp_base_delete(struct tansu_upnp_base *base)
{
    struct tansu_device *device;

    if (base == NULL)
        return;

    STAILQ_FREE_ALL(device, &base->devices, next, tansu_device_delete);
    free(base->server_name);

    tansu_upnp_base_stop(base);
    free(base);
}

tansu_status_t
tansu_upnp_base_add_device(struct tansu_upnp_base *base,
                           struct tansu_device *device)
{
    STAILQ_INSERT_TAIL(&base->devices, device, next);

    device->base = base;
    tansu_upnp_base_notify(base, device);
    tansu_device_extend_exp(device);

    return TANSU_SUCCESS;
}

void
tansu_upnp_base_notify_all(struct tansu_upnp_base *base)
{
    struct tansu_device *device;

    STAILQ_FOREACH(device, &base->devices, next) {
        tansu_upnp_base_notify(base, device);
    }
}

tansu_status_t
tansu_upnp_base_notify(struct tansu_upnp_base *base,
                       struct tansu_device *device)
{
    int n;
    const char *udn;
    struct tansu_ssdp_ad *ad;

    udn = tansu_dd_get_udn(device->dd);
    TASSERT(udn);

    ad = tansu_ssdp_ad_new(tansu_upnp_count_3_2d_k(device));
    if (ad == NULL)
        return TANSU_MEMORY_ERROR;

    n = tansu_upnp_build_3_2d_k(device, ad->nt, ad->nt_size);
    TASSERT(n == ad->nt_size);

    ad->udn = udn;
    ad->nts = TANSU_SSDP_NTS_ALIVE;
    ad->current = 0;
    ad->max_age = device->max_age;
    ad->port = base->http_port;

    tansu_ssdp_server_notify(base->ssdp, ad);

    return TANSU_SUCCESS;
}


struct tansu_service *
tansu_service_new(const char *type,
                  const char *sid,
                  struct tdom_doc *scpd,
                  void (*cb)(struct evhttp_request *,
                             struct tansu_device *,
                             struct tansu_service *,
                             const char *action,
                             void *),
                  void *cbarg)
{
    struct tansu_service *service = NULL;

    service = calloc(1, sizeof(struct tansu_service));
    if (service == NULL)
        return NULL;

    if ((service->type = strdup(type)) == NULL)
        goto out;

    if ((service->sid = strdup(sid)) == NULL)
        goto out;

    if ((service->buffer = evbuffer_new()) == NULL)
        goto out;

    service->scpd = scpd;
    service->cb = cb;
    service->cbarg = cbarg;

    return service;

out:
    tansu_service_delete(service);
    return NULL;
}

void
tansu_service_delete(struct tansu_service *service)
{
    if (service == NULL)
        return;

    free(service->type);
    free(service->sid);
    evbuffer_free(service->buffer);
    tdom_doc_delete(service->scpd);
    free(service);
}


struct tansu_device *
tansu_device_new(struct tdom_doc *dd, const char *udn)
{
    struct tansu_device *device;

    if (udn)
        if (tansu_dd_set_udn(dd, udn) != TANSU_SUCCESS)
            return NULL;

    if (tansu_dd_get_udn(dd) == NULL)
        return NULL;

    if ((device = calloc(1, sizeof(struct tansu_device))) == NULL)
        return NULL;

    if ((device->buffer = evbuffer_new()) == NULL) {
        free(device);
        return NULL;
    }
    STAILQ_INIT(&device->services);
    STAILQ_INIT(&device->devices);
    device->dd = dd;
    device->base = NULL;
    device->max_age = TANSU_MAX_AGE_DEFAULT;

    return device;
}

void
tansu_device_delete(struct tansu_device *device)
{
    struct tansu_device *d;
    struct tansu_service *s;

    if (device == NULL)
        return;

    STAILQ_FREE_ALL(s, &device->services, next, tansu_service_delete);
    STAILQ_FREE_ALL(d, &device->devices, next_embedded, tansu_device_delete);

    event_del(&device->exp_ev);
    evbuffer_free(device->buffer);
    tdom_doc_delete(device->dd);

    free(device);
}

int
tansu_device_count_device_type(struct tansu_device *device, const char *st)
{
    int count = 0;
    const char *dt;
    struct tansu_device *embedded;

    dt = tansu_dd_get_device_type(device->dd);
    TASSERT(dt);

    if (tansu_upnp_check_type(dt, st) == 0)
        count++;

    STAILQ_FOREACH(embedded, &device->devices, next_embedded) {
        dt = tansu_dd_get_device_type(embedded->dd);
        if (tansu_upnp_check_type(dt, st) == 0)
            count++;
    }

    return count;
}

int
tansu_device_count_service_type(struct tansu_device *device, const char *st)
{
    int count = 0;
    struct tansu_service *service;

    STAILQ_FOREACH(service, &device->services, next) {
        if (tansu_upnp_check_type(service->type, st) == 0)
            count++;
    }

    return count;
}

struct tansu_service_tags {
    const char *name;
    const char *format;
};

static const struct tansu_service_tags __tags[] = {
    {"SCPDURL", "/%s/%s/scpd"},
    {"controlURL", "/%s/%s/soap"},
    {"eventSubURL", "/%s/%s/sub"},
};

static tansu_status_t
tansu_device_update_url(struct tansu_device *device, const char *sid)
{
    const char *udn;
    struct tdom_elem *elem;
    char *buff;
    int len;
    int i;
    tansu_status_t result = TANSU_NOT_FOUND;

    elem = tansu_dd_get_elem(device->dd->root, "device", 0);
    if ((udn = tansu_dd_get_elem_value(elem, "UDN")) == NULL)
        return TANSU_INVALID_STATE;

    /* for new URL */
    len = 1 + strlen(udn) + 1 + strlen(sid) + 1 + 4 + 1; /* +4 means "soap" "gena" "sub" */
    if ((buff = malloc(len)) == NULL)
        return TANSU_MEMORY_ERROR;

    /* find service element */
    if ((elem = tansu_dd_get_service(elem, sid)) == NULL)
        return TANSU_NOT_FOUND;

    for (i = 0; i < TNUMELEM(__tags); i++) {
        snprintf(buff, len, __tags[i].format, udn, sid);
        if (tdom_set_elem_value(elem, __tags[i].name, buff, TANSU_NS_UPNP_DEVICE) == 0) {
            result = TANSU_SUCCESS;
        } else {
            result = TANSU_MEMORY_ERROR;
            break;
        }
    }
    free(buff);

    return result;
}

tansu_status_t
tansu_device_add_service(struct tansu_device *device,
                         const char *type,
                         const char *sid,
                         struct tdom_doc *scpd,
                         void (*cb)(struct evhttp_request *,
                                    struct tansu_device *,
                                    struct tansu_service *,
                                    const char *action,
                                    void *),
                         void *cbarg)
{
    tansu_status_t ret;
    struct tansu_service *service;

    if (device == NULL || type == NULL || sid == NULL || scpd == NULL)
        return TANSU_INVALID_ARGUMENT;

    ret = tansu_device_update_url(device, sid);
    if (ret != TANSU_SUCCESS)
        return ret;

    if ((service = tansu_service_new(type, sid, scpd, cb, cbarg)) == NULL)
        return TANSU_MEMORY_ERROR;

    STAILQ_INSERT_TAIL(&device->services, service, next);

    return TANSU_SUCCESS;
}

tansu_status_t
tansu_device_remove_service(struct tansu_device *device, const char *sid)
{
    struct tansu_service *service;

    if (device == NULL || sid == NULL)
        return TANSU_INVALID_ARGUMENT;

    STAILQ_FOREACH(service, &device->services, next) {
        if (strcmp(service->sid, sid) == 0) {
            STAILQ_REMOVE(&device->services, service, tansu_service, next);
            tansu_service_delete(service);
            return TANSU_SUCCESS;
        }
    }

    return TANSU_NOT_FOUND;
}

static void tansu_device_expired(int fd, short what, void *arg)
{
    struct tansu_device *device = (struct tansu_device *)arg;

    tansu_upnp_base_notify(device->base, device);
    tansu_device_extend_exp(device);
}

void
tansu_device_extend_exp(struct tansu_device *device)
{
    struct timeval tv = {0, 0};
    tv.tv_sec = device->max_age / 2;

    event_del(&device->exp_ev);
    evtimer_assign(&device->exp_ev, device->base->evbase,
                   tansu_device_expired, device);
    event_add(&device->exp_ev, &tv);
}

void
tansu_device_soap_cb(struct evhttp_request *req,
                     struct tansu_device *device,
                     struct tansu_service *service)
{
    const char *action;
    char *name;
    size_t len;

    if (service->cb == NULL)
        goto out;

    if ((action = evhttp_find_header(req->input_headers, "SOAPAction")) == NULL)
        goto out;

    if ((action = strrchr(action, '#')) == NULL)
        goto out;

    if ((len = strlen(action)) <= 1)
        goto out;

    if ((name = malloc(len)) == NULL)
        goto out;

    memcpy(name, &action[1], len);
    if (name[len - 2] == '"')
        name[len - 2] = '\0';

    service->cb(req, device, service, name, service->cbarg);
    free(name);
    return;

out:
    // TODO: SOAP ERROR
    evhttp_send_error(req, 500, "Internal Server Error");
}

void
tansu_device_gena_cb(struct evhttp_request *req,
                     struct tansu_device *device,
                     struct tansu_service *service)
{
    // TODO: impl

    goto out;

out:
    // TODO: GENA ERROR
    evhttp_send_error(req, 500, "Internal Server Error");
}

void
tansu_device_http_cb(struct evhttp_request *req, void *arg, int len)
{
    struct evbuffer *buffer;
    struct tansu_device *device;
    struct tansu_service *service;
    const char *uri = &req->uri[len + 1];

    printf("%s(%s) called\n", __FUNCTION__, req->uri);

    buffer = evbuffer_new(); // TODO: free
    device = (struct tansu_device *)arg;

    if (*uri == '\0') {
        tdom_writer_write_all(buffer, device->dd);
        evhttp_send_reply(req, HTTP_OK, "OK", buffer);
        goto out;
    }
    uri = &uri[1];

    STAILQ_FOREACH(service, &device->services, next) {
        len = strlen(service->sid);
        if (strncmp(uri, service->sid, len) != 0)
            continue;

        if (uri[len] != '/')
            continue;

        if (strcmp(&uri[len + 1], "scpd") == 0) {
            tdom_writer_write_all(buffer, service->scpd);
            evhttp_send_reply(req, HTTP_OK, "OK", buffer);
            goto out;
        }
        if (strcmp(&uri[len + 1], "soap") == 0) {
            tansu_device_soap_cb(req, device, service);
            goto out;
        }
        if (strcmp(&uri[len + 1], "gena") == 0) {
            tansu_device_gena_cb(req, device, service);
            goto out;
        }
        // TODO: GENA
    }

    evhttp_send_error(req, 404, "Not Found");

out:
    evbuffer_free(buffer);
}

